using System;
using System.IO;
using ZipLib.Core;

namespace ZipLib.Zip
{
    /// <summary>
    /// FastZip provides facilities for creating and extracting zip files.
    /// </summary>
    // ReSharper disable UnusedMember.Global
    public class FastZip
    // ReSharper restore UnusedMember.Global
    {
        #region Enumerations
        /// <summary>
        /// Defines the desired handling when overwriting files during extraction.
        /// </summary>
        public enum Overwrite
        {
            /// <summary>
            /// Prompt the user to confirm overwriting
            /// </summary>
            Prompt,
            /// <summary>
            /// Never overwrite files.
            /// </summary>
            Never,
            /// <summary>
            /// Always overwrite files.
            /// </summary>
            Always
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Initialise a default instance of <see cref="FastZip"/>.
        /// </summary>
        public FastZip()
        {
        }

        /// <summary>
        /// Initialise a new instance of <see cref="FastZip"/>
        /// </summary>
        /// <param name="argEvents">The <see cref="FastZipEvents">events</see> to use during operations.</param>
        public FastZip(FastZipEvents argEvents)
        {
            _events = argEvents;
        }
        #endregion

        #region Properties
        /// <summary>
        /// Get/set a value indicating wether empty directories should be created.
        /// </summary>
        public bool CreateEmptyDirectories
        {
            get { return _createEmptyDirectories; }
            set { _createEmptyDirectories = value; }
        }

#if !NETCF_1_0
        /// <summary>
        /// Get / set the password value.
        /// </summary>
        public string Password
        {
            get { return _password; }
            set { _password = value; }
        }
#endif

        /// <summary>
        /// Get or set the <see cref="INameTransform"></see> active when creating Zip files.
        /// </summary>
        /// <seealso cref="EntryFactory"></seealso>
        public INameTransform NameTransform
        {
            get { return _entryFactory.NameTransform; }
            set
            {
                _entryFactory.NameTransform = value;
            }
        }

        /// <summary>
        /// Get or set the <see cref="IEntryFactory"></see> active when creating Zip files.
        /// </summary>
        public IEntryFactory EntryFactory
        {
            get { return _entryFactory; }
            set
            {
                if (value == null)
                {
                    _entryFactory = new ZipEntryFactory();
                }
                else
                {
                    _entryFactory = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the setting for <see cref="UseZip64">Zip64 handling when writing.</see>
        /// </summary>
        /// <remarks>
        /// The default value is dynamic which is not backwards compatible with old
        /// programs and can cause problems with XP's built in compression which cant
        /// read Zip64 archives. However it does avoid the situation were a large file
        /// is added and cannot be completed correctly.
        /// NOTE: Setting the size for entries before they are added is the best solution!
        /// By default the EntryFactory used by FastZip will set fhe file size.
        /// </remarks>
        public UseZip64 UseZip64
        {
            get { return _useZip64; }
            set { _useZip64 = value; }
        }

        /// <summary>
        /// Get/set a value indicating wether file dates and times should 
        /// be restored when extracting files from an archive.
        /// </summary>
        /// <remarks>The default value is true.</remarks>
        public bool RestoreDateTimeOnExtract
        {
            get
            {
                return _restoreDateTimeOnExtract;
            }
            set
            {
                _restoreDateTimeOnExtract = value;
            }
        }

        /// <summary>
        /// Get/set a value indicating wether file attributes should
        /// be restored during extract operations
        /// </summary>
        /// <remarks>The default value is true.</remarks>
        public bool RestoreAttributesOnExtract
        {
            get { return _restoreAttributesOnExtract; }
            set { _restoreAttributesOnExtract = value; }
        }
        #endregion

        #region Delegates
        /// <summary>
        /// Delegate called when confirming overwriting of files.
        /// </summary>
        public delegate bool ConfirmOverwriteDelegate(string argFileName);
        #endregion

        #region CreateZip
        /// <summary>
        /// Create a zip file.
        /// </summary>
        /// <param name="argZipFileName">The name of the zip file to create.</param>
        /// <param name="argSourceDirectory">The directory to source files from.</param>
        /// <param name="argRecurse">True to recurse directories, false for no recursion.</param>
        /// <param name="argFileFilter">The <see cref="PathFilter">file filter</see> to apply.</param>
        /// <param name="argDirectoryFilter">The <see cref="PathFilter">directory filter</see> to apply.</param>
        public void CreateZip(string argZipFileName, string argSourceDirectory,
            bool argRecurse, string argFileFilter, string argDirectoryFilter)
        {
            CreateZip(File.Create(argZipFileName), argSourceDirectory, argRecurse, argFileFilter, argDirectoryFilter);
        }

        /// <summary>
        /// Create a zip file/archive.
        /// </summary>
        /// <param name="argZipFileName">The name of the zip file to create.</param>
        /// <param name="argSourceDirectory">The directory to obtain files and directories from.</param>
        /// <param name="argRecurse">True to recurse directories, false for no recursion.</param>
        /// <param name="argFileFilter">The file filter to apply.</param>
        public void CreateZip(string argZipFileName, string argSourceDirectory, bool argRecurse, string argFileFilter)
        {
            CreateZip(File.Create(argZipFileName), argSourceDirectory, argRecurse, argFileFilter, null);
        }

        /// <summary>
        /// Create a zip archive sending output to the <paramref name="argOutputStream"/> passed.
        /// </summary>
        /// <param name="argOutputStream">The stream to write archive data to.</param>
        /// <param name="argSourceDirectory">The directory to source files from.</param>
        /// <param name="argRecurse">True to recurse directories, false for no recursion.</param>
        /// <param name="argFileFilter">The <see cref="PathFilter">file filter</see> to apply.</param>
        /// <param name="argDirectoryFilter">The <see cref="PathFilter">directory filter</see> to apply.</param>
        /// <remarks>The <paramref name="argOutputStream"/> is closed after creation.</remarks>
        public void CreateZip(Stream argOutputStream, string argSourceDirectory, bool argRecurse, string argFileFilter, string argDirectoryFilter)
        {
            NameTransform = new ZipNameTransform(argSourceDirectory);
            _sourceDirectory = argSourceDirectory;

            using (_outputStream = new ZipOutputStream(argOutputStream))
            {

#if !NETCF_1_0
                if (_password != null)
                {
                    _outputStream.Password = _password;
                }
#endif

                _outputStream.UseZip64 = UseZip64;
                FileSystemScanner scanner = new FileSystemScanner(argFileFilter, argDirectoryFilter);
                scanner.ProcessFile += ProcessFile;
                if (CreateEmptyDirectories)
                {
                    scanner.ProcessDirectory += ProcessDirectory;
                }

                if (_events != null)
                {
                    if (_events.FileFailure != null)
                    {
                        scanner.FileFailure += _events.FileFailure;
                    }

                    if (_events.DirectoryFailure != null)
                    {
                        scanner.DirectoryFailure += _events.DirectoryFailure;
                    }
                }

                scanner.Scan(argSourceDirectory, argRecurse);
            }
        }

        #endregion

        #region ExtractZip
        /// <summary>
        /// Extract the contents of a zip file.
        /// </summary>
        /// <param name="argZipFileName">The zip file to extract from.</param>
        /// <param name="argTargetDirectory">The directory to save extracted information in.</param>
        /// <param name="argFileFilter">A filter to apply to files.</param>
        public void ExtractZip(string argZipFileName, string argTargetDirectory, string argFileFilter)
        {
            ExtractZip(argZipFileName, argTargetDirectory, Overwrite.Always, null, argFileFilter, null, _restoreDateTimeOnExtract);
        }

        /// <summary>
        /// Extract the contents of a zip file.
        /// </summary>
        /// <param name="argZipFileName">The zip file to extract from.</param>
        /// <param name="argTargetDirectory">The directory to save extracted information in.</param>
        /// <param name="argOverwrite">The style of <see cref="Overwrite">overwriting</see> to apply.</param>
        /// <param name="argConfirmDelegate">A delegate to invoke when confirming overwriting.</param>
        /// <param name="argFileFilter">A filter to apply to files.</param>
        /// <param name="argDirectoryFilter">A filter to apply to directories.</param>
        /// <param name="argRestoreDateTime">Flag indicating whether to restore the date and time for extracted files.</param>
        public void ExtractZip(string argZipFileName, string argTargetDirectory,
                               Overwrite argOverwrite, ConfirmOverwriteDelegate argConfirmDelegate,
                               string argFileFilter, string argDirectoryFilter, bool argRestoreDateTime)
        {
            Stream inputStream = File.Open(argZipFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
            ExtractZip(inputStream, argTargetDirectory, argOverwrite, argConfirmDelegate, argFileFilter, argDirectoryFilter, argRestoreDateTime, true);
        }

        /// <summary>
        /// Extract the contents of a zip file held in a stream.
        /// </summary>
        /// <param name="argInputStream">The seekable input stream containing the zip to extract from.</param>
        /// <param name="argTargetDirectory">The directory to save extracted information in.</param>
        /// <param name="argOverwrite">The style of <see cref="Overwrite">overwriting</see> to apply.</param>
        /// <param name="argConfirmDelegate">A delegate to invoke when confirming overwriting.</param>
        /// <param name="argFileFilter">A filter to apply to files.</param>
        /// <param name="argDirectoryFilter">A filter to apply to directories.</param>
        /// <param name="argRestoreDateTime">Flag indicating whether to restore the date and time for extracted files.</param>
        /// <param name="argIsStreamOwner">Flag indicating whether the inputStream will be closed by this method.</param>
        public void ExtractZip(Stream argInputStream, string argTargetDirectory,
                       Overwrite argOverwrite, ConfirmOverwriteDelegate argConfirmDelegate,
                       string argFileFilter, string argDirectoryFilter, bool argRestoreDateTime,
                       bool argIsStreamOwner)
        {
            if ((argOverwrite == Overwrite.Prompt) && (argConfirmDelegate == null))
            {
                throw new ArgumentNullException("argConfirmDelegate");
            }

            _continueRunning = true;
            _overwrite = argOverwrite;
            _confirmDelegate = argConfirmDelegate;
            _extractNameTransform = new WindowsNameTransform(argTargetDirectory);

            _fileFilter = new NameFilter(argFileFilter);
            _directoryFilter = new NameFilter(argDirectoryFilter);
            _restoreDateTimeOnExtract = argRestoreDateTime;

            using (_zipFile = new ZipFile(argInputStream))
            {

#if !NETCF_1_0
                if (_password != null)
                {
                    _zipFile.Password = _password;
                }
#endif
                _zipFile.IsStreamOwner = argIsStreamOwner;
                System.Collections.IEnumerator enumerator = _zipFile.GetEnumerator();
                while (_continueRunning && enumerator.MoveNext())
                {
                    ZipEntry entry = (ZipEntry)enumerator.Current;
                    if (entry.IsFile)
                    {
                        // TODO Path.GetDirectory can fail here on invalid characters.
                        if (_directoryFilter.IsMatch(Path.GetDirectoryName(entry.Name)) && _fileFilter.IsMatch(entry.Name))
                        {
                            ExtractEntry(entry);
                        }
                    }
                    else if (entry.IsDirectory)
                    {
                        if (_directoryFilter.IsMatch(entry.Name) && CreateEmptyDirectories)
                        {
                            ExtractEntry(entry);
                        }
                    }
                    // Do nothing for volume labels etc...

                }
            }
        }
        #endregion

        #region Internal Processing
        void ProcessDirectory(object argSender, DirectoryEventArgs argEvtArgs)
        {
            if (!argEvtArgs.HasMatchingFiles && CreateEmptyDirectories)
            {
                if (_events != null)
                {
                    _events.OnProcessDirectory(argEvtArgs.Name, argEvtArgs.HasMatchingFiles);
                }

                if (argEvtArgs.ContinueRunning)
                {
                    if (argEvtArgs.Name != _sourceDirectory)
                    {
                        ZipEntry entry = _entryFactory.MakeDirectoryEntry(argEvtArgs.Name);
                        _outputStream.PutNextEntry(entry);
                    }
                }
            }
        }

        void ProcessFile(object argSender, ScanEventArgs argEvtArgs)
        {
            if ((_events != null) && (_events.ProcessFile != null))
            {
                _events.ProcessFile(argSender, argEvtArgs);
            }

            if (argEvtArgs.ContinueRunning)
            {
                try
                {
                    // The open below is equivalent to OpenRead which gaurantees that if opened the 
                    // file will not be changed by subsequent openers, but precludes opening in some cases
                    // were it could succeed.
                    using (FileStream stream = File.Open(argEvtArgs.Name, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        ZipEntry entry = _entryFactory.MakeFileEntry(argEvtArgs.Name);
                        _outputStream.PutNextEntry(entry);
                        AddFileContents(argEvtArgs.Name, stream);
                    }
                }
                catch (Exception ex)
                {
                    if (_events != null)
                    {
                        _continueRunning = _events.OnFileFailure(argEvtArgs.Name, ex);
                    }
                    else
                    {
                        _continueRunning = false;
                        throw;
                    }
                }
            }
        }

        void AddFileContents(string argName, Stream argStream)
        {
            if (argStream == null)
            {
                throw new ArgumentNullException("argStream");
            }

            if (_buffer == null)
            {
                _buffer = new byte[4096];
            }

            if ((_events != null) && (_events.Progress != null))
            {
                StreamUtils.Copy(argStream, _outputStream, _buffer,
                    _events.Progress, _events.ProgressInterval, this, argName);
            }
            else
            {
                StreamUtils.Copy(argStream, _outputStream, _buffer);
            }

            if (_events != null)
            {
                _continueRunning = _events.OnCompletedFile(argName);
            }
        }

        void ExtractFileEntry(ZipEntry argEntry, string argTargetName)
        {
            bool proceed = true;
            if (_overwrite != Overwrite.Always)
            {
                if (File.Exists(argTargetName))
                {
                    if ((_overwrite == Overwrite.Prompt) && (_confirmDelegate != null))
                    {
                        proceed = _confirmDelegate(argTargetName);
                    }
                    else
                    {
                        proceed = false;
                    }
                }
            }

            if (proceed)
            {
                if (_events != null)
                {
                    _continueRunning = _events.OnProcessFile(argEntry.Name);
                }

                if (_continueRunning)
                {
                    try
                    {
                        using (FileStream outputStream = File.Create(argTargetName))
                        {
                            if (_buffer == null)
                            {
                                _buffer = new byte[4096];
                            }
                            if ((_events != null) && (_events.Progress != null))
                            {
                                StreamUtils.Copy(_zipFile.GetInputStream(argEntry), outputStream, _buffer,
                                    _events.Progress, _events.ProgressInterval, this, argEntry.Name, argEntry.Size);
                            }
                            else
                            {
                                StreamUtils.Copy(_zipFile.GetInputStream(argEntry), outputStream, _buffer);
                            }

                            if (_events != null)
                            {
                                _continueRunning = _events.OnCompletedFile(argEntry.Name);
                            }
                        }

#if !NETCF_1_0 && !NETCF_2_0
                        if (_restoreDateTimeOnExtract)
                        {
                            File.SetLastWriteTime(argTargetName, argEntry.DateTime);
                        }

                        if (RestoreAttributesOnExtract && argEntry.IsDOSEntry && (argEntry.ExternalFileAttributes != -1))
                        {
                            FileAttributes fileAttributes = (FileAttributes)argEntry.ExternalFileAttributes;
                            // TODO: FastZip - Setting of other file attributes on extraction is a little trickier.
                            fileAttributes &= (FileAttributes.Archive | FileAttributes.Normal | FileAttributes.ReadOnly | FileAttributes.Hidden);
                            File.SetAttributes(argTargetName, fileAttributes);
                        }
#endif
                    }
                    catch (Exception ex)
                    {
                        if (_events != null)
                        {
                            _continueRunning = _events.OnFileFailure(argTargetName, ex);
                        }
                        else
                        {
                            _continueRunning = false;
                            throw;
                        }
                    }
                }
            }
        }

        void ExtractEntry(ZipEntry argEntry)
        {
            bool doExtraction = argEntry.IsCompressionMethodSupported();
            string targetName = argEntry.Name;

            if (doExtraction)
            {
                if (argEntry.IsFile)
                {
                    targetName = _extractNameTransform.TransformFile(targetName);
                }
                else if (argEntry.IsDirectory)
                {
                    targetName = _extractNameTransform.TransformDirectory(targetName);
                }

                doExtraction = !((targetName == null) || (targetName.Length == 0));
            }

            // TODO: Fire delegate/throw exception were compression method not supported, or name is invalid?

            string dirName = null;

            if (doExtraction)
            {
                if (argEntry.IsDirectory)
                {
                    dirName = targetName;
                }
                else
                {
                    dirName = Path.GetDirectoryName(Path.GetFullPath(targetName));
                }
            }

            if (doExtraction && !String.IsNullOrWhiteSpace(dirName))
            {
                if (!Directory.Exists(dirName))
                {
                    if (!argEntry.IsDirectory || CreateEmptyDirectories)
                    {
                        try
                        {
                            Directory.CreateDirectory(dirName);
                        }
                        catch (Exception ex)
                        {
                            doExtraction = false;
                            if (_events != null)
                            {
                                if (argEntry.IsDirectory)
                                {
                                    _continueRunning = _events.OnDirectoryFailure(targetName, ex);
                                }
                                else
                                {
                                    _continueRunning = _events.OnFileFailure(targetName, ex);
                                }
                            }
                            else
                            {
                                _continueRunning = false;
                                throw;
                            }
                        }
                    }
                }
            }

            if (doExtraction && argEntry.IsFile)
            {
                ExtractFileEntry(argEntry, targetName);
            }
        }

        static int MakeExternalAttributes(FileInfo argInfo)
        {
            return (int)argInfo.Attributes;
        }

#if NET_1_0 || NET_1_1 || NETCF_1_0
		static bool NameIsValid(string name)
		{
			return (name != null) &&
				(name.Length > 0) &&
				(name.IndexOfAny(Path.InvalidPathChars) < 0);
		}
#else
        static bool NameIsValid(string argName)
        {
            return (!string.IsNullOrEmpty(argName)) &&
                (argName.IndexOfAny(Path.GetInvalidPathChars()) < 0);
        }
#endif
        #endregion

        #region Instance Fields
        bool _continueRunning;
        byte[] _buffer;
        ZipOutputStream _outputStream;
        ZipFile _zipFile;
        string _sourceDirectory;
        NameFilter _fileFilter;
        NameFilter _directoryFilter;
        Overwrite _overwrite;
        ConfirmOverwriteDelegate _confirmDelegate;

        bool _restoreDateTimeOnExtract = true;
        bool _restoreAttributesOnExtract = true;
        bool _createEmptyDirectories;
        FastZipEvents _events;
        IEntryFactory _entryFactory = new ZipEntryFactory();
        INameTransform _extractNameTransform;
        UseZip64 _useZip64 = UseZip64.Dynamic;

#if !NETCF_1_0
        string _password;
#endif

        #endregion
    }
}
